using System;
using System.Collections.Generic;
using System.Linq;
using Xunit.Sdk;

namespace SubSpec
{
    static class SpecificationContext
    {
        [ThreadStatic]
        private static bool _threadStaticInitialized;
        [ThreadStatic]
        private static SpecificationPrimitive<ContextDelegate> _context;
        [ThreadStatic]
        private static SpecificationPrimitive<Action> _do;
        [ThreadStatic]
        private static List<SpecificationPrimitive<Action>> _asserts;
        [ThreadStatic]
        private static Func<IEnumerable<SpecificationPrimitive<Action>>> _observations;
        [ThreadStatic]
        private static List<SpecificationPrimitive<Action>> _skips;
        [ThreadStatic]
        private static List<Action> _exceptions;

        private static void Reset()
        {
            _exceptions = new List<Action>();
            _context = null;
            _do = null;
            _asserts = new List<SpecificationPrimitive<Action>>();
            _observations = () => Enumerable.Empty<SpecificationPrimitive<Action>>();
            _skips = new List<SpecificationPrimitive<Action>>();
        }

        private static void EnsureThreadStaticInitialized()
        {
            if (_threadStaticInitialized)
                return;

            Reset();
            _threadStaticInitialized = true;
        }

        public static ISpecificationPrimitive Context( string message, ContextDelegate arrange )
        {
            EnsureThreadStaticInitialized();

            if (_context == null)
                _context = new SpecificationPrimitive<ContextDelegate>( message, arrange );
            else
                _exceptions.Add( () => { throw new InvalidOperationException( "Cannot have more than one Context statement in a specification" ); } );

            return _context;
        }

        public static ISpecificationPrimitive Do( string message, Action doAction )
        {
            EnsureThreadStaticInitialized();

            if (_do == null)
                _do = new SpecificationPrimitive<Action>( message, doAction );
            else
                _exceptions.Add( () => { throw new InvalidOperationException( "Cannot have more than one Do statement in a specification" ); } );

            return _do;
        }

        public static ISpecificationPrimitive Assert( string message, Action assertAction )
        {
            EnsureThreadStaticInitialized();

            SpecificationPrimitive<Action> assert = new SpecificationPrimitive<Action>( message, assertAction );
            _asserts.Add( assert );

            return assert;
        }

        public static ISpecificationPrimitive Observation( string message, Action observationAction )
        {
            EnsureThreadStaticInitialized();

            var previousObservations = _observations;
            var observation = new SpecificationPrimitive<Action>( message, observationAction );
            _observations = () => previousObservations().Append( observation );

            return observation;
        }

        public static void SetObservations( Func<IEnumerable<SpecificationPrimitive<Action>>> lazyEnumerable )
        {
            _observations = lazyEnumerable;
        }

        public static ISpecificationPrimitive Todo( string message, Action skippedAction )
        {
            EnsureThreadStaticInitialized();

            SpecificationPrimitive<Action> skip = new SpecificationPrimitive<Action>( message, skippedAction );
            _skips.Add( skip );

            return skip;
        }

        private static string PrepareName()
        {
            string name = _context.Message;

            if (_do != null)
                name += " " + _do.Message;
            return name;
        }

        internal static IEnumerable<ITestCommand> ToTestCommands( IMethodInfo method )
        {
            EnsureThreadStaticInitialized();

            try
            {
                var validationException = ValidateSpecification( method );
                if (validationException != null)
                {
                    yield return validationException;
                    yield break;
                }

                int testsReturned = 0;
                string name = PrepareName();

                var ax = new AssertExecutor( _context, _do, _asserts );
                foreach (var item in ax.AssertCommands( name, method ))
                {
                    yield return item;
                    testsReturned++;
                }

                var ox = new ObservationExecutor( _context, _do, _observations );
                foreach (var item in ox.ObservationCommands( name, method ))
                {
                    yield return item;
                    testsReturned++;
                }

                foreach (var item in SkipCommands( name, method ))
                {
                    yield return item;
                    testsReturned++;
                }

                if (testsReturned == 0)
                    yield return new ExceptionTestCommand( method, new InvalidOperationException( "Must have at least one Assert or Observation in each specification" ) );
            }
            finally
            {
                Reset();
            }
        }

        private static ExceptionTestCommand ValidateSpecification( IMethodInfo method )
        {
            if (_context == null)
                _exceptions.Add( () => { throw new InvalidOperationException( "Must have a Context in each specification" ); } );

            // check if we have any recorded exceptions
            if (_exceptions.Count > 0)
            {
                try
                {
                    _exceptions[0](); // throw the first recorded exception, preserves stacktraces nicely.
                }
                catch (Exception ex)
                {
                    return new ExceptionTestCommand( method, ex );
                }
            }

            return null;
        }

        private static IEnumerable<ITestCommand> SkipCommands( string name, IMethodInfo method )
        {
            foreach (var kvp in _skips)
                yield return new SkipCommand( method, name + ", " + kvp.Message, "Action is Todo (instead of Observation or Assert)" );
        }
    }

    internal static partial class Mixin
    {
        public static IEnumerable<T> Append<T>( this IEnumerable<T> source, T item )
        {
            foreach (var x in source)
                yield return x;

            yield return item;
        }
    }
}
